iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Vibe Coding

做一個團購後端,順便搞懂那些事系列 第 26 篇

Day 26|通知會漏、會亂序,訊息裡該放什麼?

  • 分享至 

  • xImage
  •  

使用者下單、刪除品項,或訂單結算扣款後,伺服器透過 WebSocket 推送一則訊息,讓前端更新畫面上的可用餘額,然而,通知並不保證送達,在交易 commit 後、訊息送出前,機器可能會當掉,且跨機器轉送使用的 Redis Pub/Sub 並不保存訊息,使用者離線或正在重連時也可能漏收。

因此,畫面上的可用餘額必須能在收到後續通知時恢復正確,而不能因為漏掉一則訊息就一直算錯。

問題一:漏掉一則通知,畫面就會一直算錯?

如果訊息只帶變動量,由前端自行加減,漏掉任何一則都會影響後續計算:
舉例:

事件 訊息 前端畫面 資料庫
初始 5000 5000
下單 300 -300(漏掉) 5000 4700
下單 200 -200 4800 4500

漏掉第一則後,前端就會一直差 300。

因此,本專案初期版本的 BalanceMessage 傳遞的是計算完成的可用餘額,而不是變動量:

@Data
public class BalanceMessage {

    private final String type = "BALANCE_UPDATED";
    private long availableBalance;
    private String reason;

    public BalanceMessage(long availableBalance, String reason) { ... }
}

當時的 BalanceChangeNotifier 會在交易 commit 後重新從資料庫計算可用餘額,再送出通知:

long available = user.getBalance() - orderItemMapper.getFrozenAmount(event.userId());
notificationService.sendBalanceUpdate(event.userId(), available, event.reason());

問題二:收到的明明是最新狀態,為什麼畫面還是錯的?

前面解決了漏訊息造成的累積誤差,卻還有另一個問題:「後送出的訊息,不一定比較新。」

假設 Alice 的可用餘額是 5000,接著連續下單兩筆各 1000,下單時會鎖住 users 的資料列,因此兩筆交易依序 commit,但是 commit 後重新計算餘額與送出通知,並不在這個鎖的保護範圍內:

https://ithelp.ithome.com.tw/upload/images/20261010/20168667XdMisXzSNj.png

結果是舊的 4000 晚於新的 3000 抵達前端,畫面反而被更新成錯誤的數字。

即使解決這個問題,訊息送出後仍有其他可能打亂順序的地方,通知從計算餘額、經過 Redis 傳遞,到最後送達瀏覽器,會經過多個處理環節,不同訊息可能由不同執行緒處理,因此即使較新的餘額先送出,也不能保證它一定會先抵達前端。

https://ithelp.ithome.com.tw/upload/images/20261010/201686675RyiNxryTT.png

至於 preservePublishOrder(Spring 用來控制同一個 WebSocket session 的訊息發布順序的設定),目前專案沒有啟用。因此,只調整其中一處的處理順序,還不足以保證整條傳送路徑都不會亂序。

解決方法:推播只通知變化,餘額重新查詢

既然通知可能亂序,就不應直接相信訊息裡的餘額,於是改成讓推播只通知前端「餘額已變動」,不再附帶數字:

@Data
public class BalanceMessage {

    private final String type = "BALANCE_CHANGED";
    private final String reason;
}

前端收到 BALANCE_CHANGED 後,再呼叫 /order/get_user_account 查詢最新的可用餘額。即使較舊的信號晚到,查詢取得的仍是資料庫當下的餘額,不會因為信號順序不同,就把舊數字寫回畫面。

為了統一處理方式,下單、刪除品項與結算扣款後,都會發布 BalanceChangedEvent,再由 BalanceChangeNotifier 等待交易 commit 後送出信號。即使漏掉一則信號,只要後續還有信號觸發查詢,前端就能重新取得正確的餘額,當然了,這麼做付出的代價是每次收到信號,都會多一次 HTTP 查詢。

而結算結果通知則有不同的需求:考量到使用者可能離線,錯過的通知不能只靠下一次餘額變動補回,因此,結算成功或失敗的通知會在對應的資料庫交易中寫入 notifications 表,推播時附上 notificationId。使用者重新連線後,可以呼叫 /user/get_unread_notifications 取得尚未讀取的通知,再由前端利用 notificationId 去除重複訊息。

推播負責提醒前端資料有變化,實際數值以重新查詢的結果為準;需要保留的結算通知則存入資料庫,確保離線後仍有機會補回。


上一篇
Day 25|多台機器時,WebSocket 通知送不到
下一篇
Day 27|登入 API 被暴力猜密碼,該限制誰?
系列文
做一個團購後端,順便搞懂那些事 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言